package com.google.code.guava.beans;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.beans.PropertyDescriptor;

import com.google.code.guava.reflect.Classes;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

/**
 * This represents a bean property bound to an owner instance. All get() and
 * set() operations use the bean property's Getter and / or Setter and invoke
 * them on the owner instance.
 * 
 * @param <T>
 *            the generic type
 */
public final class BeanProperty<T> extends BaseProperty<T>{

    /**
     * Provides a type safe generic view of this property. Usage:
     * 
     * <pre>
     * BeanProperty<?> someProperty = // initialize this somehow
     * if(String.class.equals(someProperty.getType())){
     *     someProperty.asType(String.class).setValue("Hello World");
     * }
     * </pre>
     * 
     * @param <S>
     * @param targetType
     *            the target class
     * @return this property, casted to the supplied generic type
     * @exception IllegalArgumentException
     *                if the supplied type is not compatible with
     *                {@link #getType()}
     */
    @SuppressWarnings("unchecked" /* checked at runtime */)
    public <S> BeanProperty<S> asType(final Class<S> targetType){
        checkArgument(targetType.isAssignableFrom(getType())
                      || Classes.equivalentWhenAutoBoxing(targetType, getType()),
            "Property %s is not compatible with return type %s",
            getName(),
            getType());
        return (BeanProperty<S>) this;
    }

    private static final class BeanPropertyFunction implements
    Function<PropertyDescriptor, BeanProperty<?>>{

        public static Function<? super PropertyDescriptor, ? extends BeanProperty<?>> forBean(
            final Object bean){
            return new BeanPropertyFunction(bean);
        }

        private final Object bean;

        private BeanPropertyFunction(final Object bean){
            this.bean = bean;
        }

        public BeanProperty<?> apply(final PropertyDescriptor descriptor){
            return new BeanProperty<Object>(
                bean, Properties.getPropertyfromDescriptor(descriptor));
        }
    }

    /**
     * Return a list of all properties of a given bean.
     * 
     * @exception RuntimeException
     *                if introspection fails
     * @exception NullPointerException
     *                if bean is null
     */
    public static ImmutableList<BeanProperty<?>> listProperties(
        final Object bean){

        return ImmutableList.copyOf(Lists.transform(
            Introspection.listPropertyDescriptors(checkNotNull(bean)),
            BeanPropertyFunction.forBean(bean)
        ));
    }

    private final Object owner;

    private final Property<T> property;

    BeanProperty(final Object owner, final Property<T> property){
        this.owner = owner;
        this.property = property;
    }

    @Override
    public String getName(){
        return property.getName();
    }

    @Override
    public Class<T> getType(){
        return property.getType();
    }

    /**
     * Return the bean property value.
     */
    public T getValue(){
        return property.getValueFromGetter(owner);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isReadable(){
        return property.isReadable();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isWriteable(){
        return property.isWriteable();
    }

    /**
     * Set the bean property value.
     */
    public void setValue(final T value){
        property.setValueWithSetter(owner, value);
    }

}
